Перейти к основному содержимому

5.02. Анализ данных и научные вычисления

Разработчику Архитектору

Анализ данных и научные вычисления

Программирование на Python давно вышло за пределы сценариев автоматизации и веб-разработки, заняв ведущие позиции в области обработки информации, моделирования и принятия решений на основе данных. Эта трансформация стала возможной благодаря формированию зрелой экосистемы библиотек, объединённых общей философией эффективности, ясности и научной строгости. Среди них особое место занимают три кита: NumPy — фундамент для численных операций, Pandas — инструментарий для структурированного анализа, и Matplotlib/Seaborn/Plotly — среда визуализации. Вместе они образуют каркас, на котором строятся как академические исследования, так и промышленные решения — от прогнозирования спроса в ритейле до обработки сигналов в телемедицине.

Начнём с основания — с NumPy.


NumPy

NumPy (сокращение от Numerical Python) — это библиотека с открытым исходным кодом, задающая стандарт для выполнения высокопроизводительных числовых расчётов в Python. Её появление стало поворотным моментом в развитии языка: до внедрения NumPy Python воспринимался как удобный, но медленный инструмент, непригодный для массовых вычислений. NumPy устранил это ограничение, предоставив эффективную реализацию многомерных массивов и богатый набор функций для их обработки, написанных на C и оптимизированных на уровне аппаратуры.

Главная концепция NumPy — массив (ndarray, от n-dimensional array). Это не просто список списков, как может показаться на первый взгляд. Это строго типизированная, непрерывно размещённая в памяти структура данных фиксированного размера и однородного типа элементов. Такая организация позволяет задействовать механизмы векторизации и SIMD-инструкции процессора, что даёт прирост производительности на порядки по сравнению с классическими циклами Python. Например, сложение двух миллионов чисел через for-цикл в чистом Python займёт сотни миллисекунд, тогда как в NumPy — единицы миллисекунд.

Что такое массив NumPy и чем он отличается от списка Python

Разница между list и numpy.ndarray — в архитектуре. Список в Python — это динамический массив ссылок на объекты. Каждый элемент списка хранит не значение напрямую, а указатель на него в куче. Это обеспечивает гибкость (в одном списке могут быть целые числа, строки и функции), но влечёт накладные расходы: при операциях с числами приходится распаковывать объекты, проверять их тип, выделять память — всё это тормозит вычисления.

Массив NumPy, напротив, хранит непосредственно значения одного и того же типа (например, 64-битные целые int64 или числа с плавающей точкой float64). Память выделяется один раз, как сплошной блок. Благодаря этому интерпретатор может делегировать выполнение арифметических операций оптимизированным нативным библиотекам (вроде Intel MKL или OpenBLAS), не возвращая управление в Python-цикл на каждом шаге. Это и есть суть векторизации — замена поэлементных операций единой командой, применяемой ко всему массиву сразу.

Создание массивов

Массивы NumPy создаются через функции. Наиболее универсальный способ — вызов np.array(), принимающий в качестве аргумента почти любую последовательность: список, кортеж, даже другой массив. При этом можно явно указать желаемый тип данных (dtype), что помогает избежать неожиданного приведения типов.

import numpy as np

# Простейший одномерный массив
a = np.array([1, 2, 3, 4])

# Двумерный массив из списка списков
b = np.array([[1, 2], [3, 4]])

# Явное указание типа
c = np.array([1, 2, 3], dtype=np.float32)

Для часто встречающихся шаблонов существуют специализированные конструкторы. Например, np.zeros() создаёт массив заданной формы, заполненный нулями — это стандартный способ инициализации буферов или заготовок под будущие вычисления. Аналогично, np.ones() порождает массив из единиц; np.empty() выделяет память без инициализации (быстрее, но значения неопределены — следует заполнять явно перед использованием).

Когда требуется равномерная сетка значений, пригодится np.linspace(). Эта функция генерирует заданное количество точек, равномерно распределённых между двумя границами. В отличие от range() или np.arange(), linspace() гарантирует, что конечная точка будет включена, а шаг между элементами определяется автоматически. Это особенно удобно при построении осей для графиков или дискретизации непрерывных функций.

# 5 точек от 0 до 1 включительно: [0.0, 0.25, 0.5, 0.75, 1.0]
x = np.linspace(0, 1, num=5)

Доступ к элементам

Механизм индексации в NumPy — мощное обобщение привычных операций со списками. Для одномерного массива всё остаётся интуитивно понятным: a[0], a[-1], a[2:5]. Однако уже для двумерных (и выше) массивов открывается богатство возможностей.

Доступ к элементу двумерного массива осуществляется через два индекса: matrix[i, j]i-я строка, j-й столбец. Срезы могут применяться по каждому измерению независимо: matrix[1:3, :] — все столбцы во второй и третьей строках; matrix[:, -1] — последний столбец целиком. Более того, можно передавать массивы индексов или булевы маски: a[[0, 2, 4]] вернёт элементы с указанными позициями; a[a > 0] — все положительные элементы. Это позволяет формулировать сложные запросы к данным без циклов.

Важной особенностью является то, что срезы в NumPy возвращают представление (view) исходного массива — новую структуру метаданных, ссылающуюся на ту же область памяти. Изменение среза немедленно отражается на оригинале. Это экономит память и ускоряет работу, но требует внимания при модификации данных. При необходимости можно явно вызвать .copy().

Векторные операции и трансляция

Одно из самых сильных преимуществ NumPy — возможность выполнять арифметические операции без явных циклов. Сложение, вычитание, умножение, деление, возведение в степень и многие другие функции применяются поэлементно. Более того, операции между массивами разной формы возможны благодаря механизму трансляции (broadcasting).

Трансляция — это набор строгих правил, позволяющих «расширять» меньший массив до размера большего, не занимая дополнительную память. Например, при сложении матрицы 3×4 и вектора длины 4 вектор мысленно «копируется» вдоль строк, и операция выполняется как будто бы над двумя матрицами 3×4. Аналогично, скаляр (одиночное число) трактуется как массив произвольной формы, заполненный этим значением. Это позволяет писать компактный, выразительный код: temperature_celsius = (temperature_fahrenheit - 32) * 5 / 9 работает корректно даже если temperature_fahrenheit — многомиллионный массив.

Статистические свёртки

Помимо арифметики, NumPy предоставляет широкий набор функций для агрегации данных по измерениям. Среднее арифметическое (np.mean), стандартное отклонение (np.std), дисперсия (np.var), минимум (np.min), максимум (np.max), сумма (np.sum), произведение (np.prod) — все они могут вычисляться по всему массиву или вдоль заданной оси. Например, для двумерной матрицы np.mean(matrix, axis=0) даст среднее по столбцам (результат — одномерный массив), а axis=1 — по строкам.

Эти операции реализованы с учётом численной устойчивости: при вычислении дисперсии, например, используется поправка Бесселя (как в ddof=1 по умолчанию в np.std), если явно не указано иное. Это важно для корректной интерпретации результатов в статистическом анализе.

Практическое значение и границы

NumPy — инструментарий для вычислений. Он лежит в основе почти всех научных пакетов Python: SciPy использует его для линейной алгебры и оптимизации, scikit-learn — для внутренних представлений признаков и меток, даже TensorFlow и PyTorch в своих CPU-режимах опираются на концепции и, частично, на реализации из NumPy.

Однако стоит помнить, что NumPy не поддерживает гетерогенные данные — он плохо подходит для хранения таблиц с колонками разных типов (числа, строки, даты). Также отсутствует встроенная поддержка меток осей (имён строк и столбцов) или пропущенных значений в виде NaN (хотя np.nan существует, его обработка требует дополнительных усилий). Эти задачи решаются на следующем уровне абстракции — в Pandas.

Тем не менее, именно понимание устройства и возможностей NumPy позволяет эффективно работать с любыми числовыми данными в Python. Без него невозможно осознанное использование более высокоуровневых инструментов — они либо напрямую используют массивы NumPy внутри, либо предоставляют интерфейсы для их извлечения (df.values, df.to_numpy()).


Pandas

Если NumPy — это фундамент для чисел, то Pandas — это каркас для таблиц. Название библиотеки происходит от сокращения panel data — эконометрического термина, обозначающего наборы наблюдений, упорядоченных по двум измерениям: объектам (например, клиентам, товарам, регионам) и временным моментам (дням, месяцам, кварталам). Именно такие данные — гетерогенные, разреженные, с именованными колонками и потенциально пропущенными значениями — составляют подавляющее большинство рабочих задач: логи серверов, выгрузки CRM, финансовые отчёты, результаты A/B-тестов.

Pandas не заменяет NumPy; он надстраивается над ним. Внутреннее представление числовых колонок в Pandas — это всё те же массивы ndarray, оптимизированные на скорости. Но поверх добавляются слои абстракции: именованные оси, гибкая индексация, унифицированная обработка пропущенных данных, встроенные методы фильтрации и группировки. Это превращает техническую операцию — «найти средний чек по регионам за третий квартал» — в последовательность читаемых, почти разговорных команд.

Основные структуры: Series и DataFrame

В основе Pandas лежат две ключевые структуры данных.

Series — это одномерный индексированный массив, допускающий элементы произвольного (но одного) типа. По сути, это улучшенный словарь с целочисленной или меткой-индексом, где каждому ключу сопоставлено значение. При этом индекс не обязан быть последовательным или уникальным (хотя уникальность рекомендуется), а значения могут включать специальный маркер пропущенности — pd.NA или np.nan. Series часто возникает как результат выделения одной колонки из таблицы или агрегации по группе.

DataFrame — это двумерная табличная структура: упорядоченный набор колонок, каждая из которых является объектом Series, при этом все колонки разделяют общий индекс — набор меток строк. Это и есть программная реализация привычной электронной таблицы: строки — отдельные записи (например, заказы), колонки — их атрибуты (дата, клиент, сумма, статус). Важно, что колонки могут иметь разные типы: числа, строки, даты, булевы значения — Pandas корректно хранит и обрабатывает такие смешанные данные, что невозможно в «голом» NumPy.

Оба объекта неизменяемы по структуре: добавление новой колонки или строки требует создания нового объекта (хотя копирование данных при этом минимизировано). Это гарантирует предсказуемость и безопасность при параллельной обработке, а также облегчает отладку — любое преобразование оставляет след в виде нового имени переменной.

Загрузка и сохранение данных

Анализ начинается с получения данных, и Pandas предоставляет унифицированный интерфейс для работы с самыми распространёнными форматами.

pd.read_csv() — самая востребованная функция. Она автоматически определяет разделитель (запятая, точка с запятой, табуляция), обрабатывает кавычки и экранирование, распознаёт даты, интерпретирует пустые ячейки как пропущенные. Для нестандартных случаев доступны десятки параметров: указание кодировки (encoding='utf-8', encoding='cp1251'), явное задание типов колонок (dtype={'id': 'Int64', 'price': 'float64'}), пропуск строк заголовка или итогов, обработка тысячных разделителей (thousands=' '). Эта гибкость позволяет загружать даже «некорректные» CSV-файлы, сформированные вручную или экспортированные из устаревших систем.

Аналогично работают pd.read_excel() (поддержка .xlsx, .xls, выбор листа по имени или индексу), pd.read_json(), pd.read_sql() (прямое выполнение SQL-запроса к СУБД через SQLAlchemy), pd.read_parquet() (высокопроизводительный столбцовый формат для big data). Обратные операции — to_csv(), to_excel() и другие — обеспечивают сериализацию результатов в нужный формат для передачи коллегам, загрузки в BI-системы или архивирования.

При чтении из файлов Pandas стремится к максимальной автоматизации, но эта автоматизация не всегда совпадает с ожиданиями. Например, колонка с ID, содержащая ведущие нули ('00123'), будет интерпретирована как число 123, если не указан тип str. Колонка с датами в нестандартном формате может остаться строкой. Поэтому всегда полезно после загрузки выполнить df.info() — краткий отчёт о структуре: количество строк, тип каждой колонки, объём памяти и число непустых значений.

Подготовка данных

Реальные данные редко бывают идеальными. Пропущенные значения — правило. Pandas вводит единый механизм их представления: в числовых колонках — np.nan, в строковых и категориальных — pd.NA. Оба значения ведут себя одинаково при фильтрации и агрегации: большинство статистических функций имеют параметр skipna=True по умолчанию, то есть игнорируют пропуски.

Обнаружить их можно с помощью методов isna() и notna(), возвращающих булеву маску той же формы, что и исходный объект. На основе этой маски легко посчитать, сколько строк содержат хотя бы один пропуск (df.isna().any(axis=1).sum()), или выделить «чистые» записи (df.dropna()). Однако просто удалить строки — не всегда оптимальное решение: при большом объёме пропущенных данных это может привести к потере репрезентативности выборки.

Альтернатива — заполнение. Метод fillna() позволяет подставить константу (value=0), перенести значение из предыдущей строки (method='ffill'), из следующей (method='bfill'), либо рассчитать по соседям — например, скользящее среднее. Для числовых данных часто используется медиана или мода колонки, как более устойчивые к выбросам, чем среднее. Важно документировать выбранный подход: импутация (восстановление пропущенных значений) — это гипотеза о структуре данных, влияющая на интерпретацию результатов.

Фильтрация, трансформация и группировка

Фильтрация в Pandas основана на булевых индексах. Выражение df['age'] > 30 возвращает серию True/False, которая, будучи подана в квадратные скобки, выбирает только те строки, где условие истинно. Условия можно комбинировать с помощью побитовых операторов & (и), | (или), ~ (не), заключая каждое в скобки из-за приоритета операций: df[(df['city'] == 'Москва') & (df['income'] > 100000)].

Трансформация колонок выполняется через метод assign(), который возвращает новый DataFrame с дополнительными или изменёнными колонками, не затрагивая оригинал. Он принимает именованные аргументы, где имя — название колонки, а значение — либо константа, либо результат применения функции к существующим колонкам. Например, df.assign(discount=lambda x: x['price'] * 0.1) создаёт колонку discount, равную 10% от цены. Такой подход поощряет функциональный стиль: каждая операция — чистая функция, вход — DataFrame, выход — новый DataFrame.

Группировка — один из самых мощных инструментов. Метод groupby() разбивает данные на подмножества по значению одной или нескольких колонок (например, по региону и месяцу), после чего к каждой группе можно применить агрегирующую функцию: sum(), mean(), count(), first(), last(), а также пользовательскую через agg(). Результат — сводная таблица, где строки соответствуют уникальным комбинациям ключей группировки, а колонки — агрегированным показателям. При этом можно задавать разные функции для разных колонок: средний чек, число заказов, медианное время доставки — всё в одном вызове.

summary = sales.groupby(['region', 'quarter']).agg(
total_revenue=('amount', 'sum'),
avg_order=('amount', 'mean'),
order_count=('order_id', 'count'),
median_delivery=('delivery_days', 'median')
)

Этот код читается почти как спецификация бизнес-требования, что делает его эффективным в исполнении и понятным при ревью.

Интеграция с визуализацией

Хотя Pandas не является библиотекой для построения графиков, он предоставляет удобный метод .plot(), который, по сути, является «обёрткой» над Matplotlib. Вызов df.plot(x='date', y='revenue', kind='line') построит временной ряд, df['category'].value_counts().plot(kind='bar') — столбчатую диаграмму распределения категорий. Это позволяет быстро получить первичное визуальное представление о данных без перехода в отдельный инструмент.

Такая интеграция сознательно ограничена: Pandas не стремится конкурировать с Matplotlib, Seaborn или Plotly в гибкости оформления. Его цель — обеспечить мост между анализом и визуализацией, чтобы исследователь мог, не прерывая поток мысли, перейти от расчёта статистик к их графическому отображению. Для сложных, публикуемых диаграмм всё равно потребуется более тонкая настройка через низкоуровневые библиотеки — но Pandas упрощает подготовку данных для этих библиотек: агрегированные таблицы, отфильтрованные выборки, сгруппированные сводки.

Практическое значение и границы

Pandas — это инструмент для аналитика, а не для инженера инфраструктуры. Он оптимизирован под интерактивную работу: исследование, итеративное уточнение гипотез, прототипирование отчётов. Для обработки данных объёмом в десятки гигабайт, требующей распределённых вычислений, используются другие решения: Dask (имитация API Pandas на кластере), Modin (ускорение через параллелизацию), PySpark (интеграция с экосистемой Apache Spark).

Тем не менее, знание Pandas необходимо даже в этих сценариях: Dask и Modin намеренно копируют его интерфейс, чтобы снизить порог входа. Более того, на этапе проектирования пайплайнов часто сначала пишут прототип на Pandas (на подвыборке), а затем переносят логику в распределённую среду. Таким образом, Pandas остаётся языком описания аналитических задач — стандартом де-факто для формулировки того, что нужно сделать с данными.


Визуализация данных

Визуализация — это еотъемлемая часть анализа. Человеческий мозг обрабатывает визуальную информацию на порядки быстрее, чем числовую таблицу; графики позволяют мгновенно выявлять тренды, аномалии, кластеры и корреляции, которые могли бы остаться незамеченными при простом просмотре статистик. Однако эффективная визуализация требует сознательного проектирования: выбор типа диаграммы, шкалы, цветовой палитры, подписей — всё это влияет на достоверность и убедительность выводов. В Python этот процесс поддерживается тремя взаимосвязанными библиотеками, каждая из которых решает свою задачу в иерархии абстракций.

Matplotlib

Matplotlib — старейшая и наиболее фундаментальная библиотека для построения графиков в Python. Её архитектура, вдохновлённая MATLAB, задаёт базовые понятия, на которых строятся все остальные инструменты: фигура (Figure) как контейнер верхнего уровня, оси (Axes) как область для нанесения данных, холст (Canvas) как поверхность рендеринга. Именно Matplotlib управляет шрифтами, раскладкой элементов, экспортом в PNG, PDF, SVG и другими форматами.

Ключевая особенность Matplotlib — гибкость через многоуровневый API. На самом низком — процедурный интерфейс pyplot (plt.plot(), plt.xlabel()), имитирующий MATLAB: простой для старта, но быстро становящийся громоздким при построении сложных композиций. На высшем — объектно-ориентированный подход, где явно создаются экземпляры Figure и Axes, и все операции выполняются через их методы (ax.plot(), ax.set_xlabel()). Этот стиль требует больше кода, но даёт полный контроль над каждым элементом графика, позволяет встраивать графики в GUI-приложения и обеспечивает воспроизводимость при автоматической генерации отчётов.

Стандартные стили Matplotlib консервативны, шрифты могут выглядеть устаревшими, цветовые палитры — неоптимальными для восприятия. Но именно эта «нейтральность» — преимущество: библиотека не навязывает интерпретацию, а предоставляет холст. Все детали — от толщины линий до расположения легенды — настраиваются явно, что делает графики предсказуемыми и пригодными для публикации в научных журналах, где строгие требования к форматированию.

Практически все высокоуровневые библиотеки визуализации (включая Pandas .plot()) в качестве бэкенда используют Matplotlib. Это означает, что навыки работы с его объектной моделью остаются востребованными даже при использовании более удобных обёрток: зная, как устроен Axes, можно донастроить любой график, построенный через Seaborn или Pandas.

Seaborn

Если Matplotlib — инструментарий для построения графиков, то Seaborn — фреймворк для анализа через графики. Его цель — облегчить визуальное исследование структуры данных, особенно многомерных: распределений, корреляций, отношений между категориями и непрерывными переменными. Seaborn берёт за основу концепции из статистической графики (statistics graphics), заложенные в пакеты вроде R’s ggplot2, и реализует их в Pythonic-стиле.

Главная сила Seaborn — в декларативности. Вместо того чтобы вручную вычислять гистограммы, ящики с усами или линии тренда, исследователь описывает какие переменные и в каком отношении он хочет увидеть. Например, вызов sns.scatterplot(data=df, x='income', y='spending', hue='region') автоматически:

  • построит точки на плоскости «доход–расход»,
  • раскрасит их по значениям колонки region,
  • добавит легенду с названиями регионов,
  • выберет воспринимаемую цветовую палитру для категорий,
  • настроит подписи осей на основе имён колонок.

Аналогично, sns.boxplot(x='category', y='price', data=df) построит ящики с усами, автоматически вычислив медиану, квартили и потенциальные выбросы для каждой категории. Для парных отношений служит pairplot(), для матриц корреляций — heatmap(), для распределений — histplot() и kdeplot(). Все эти функции интегрированы с DataFrame Pandas: они принимают весь фрейм и имена колонок, а не сырые массивы NumPy.

Ещё одна ключевая особенность — дизайн по умолчанию. Seaborn поставляется с набором стилей (whitegrid, dark, ticks) и цветовых палитр (viridis, rocket, husl), оптимизированных для восприятия: контрастных, устойчивых к цветовой слепоте, эстетически сбалансированных. Благодаря этому даже простой вызов даёт график, пригодный для включения в презентацию — без дополнительной настройки шрифтов, толщин линий или отступов.

Однако важно понимать границы Seaborn: он не предназначен для построения нестандартных, уникальных визуализаций (например, карт, диаграмм Санки, 3D-поверхностей). Его сфера — стандартные статистические графики, но максимально удобно и максимально информативно. Для нестандартных задач возвращаются к Matplotlib или специализированным библиотекам.

Plotly

Там, где Matplotlib отвечает за статичные отчёты, а Seaborn — за исследовательский анализ, Plotly берёт на себя сценарии коммуникации и демонстрации. Его ключевая отличительная черта — интерактивность: увеличение области графика, наведение курсора для отображения точных значений, включение/отключение серий, анимация во времени — всё это работает «из коробки» в веб-браузере.

Plotly изначально разрабатывался как JavaScript-библиотека (Plotly.js), и её Python-обёртка (plotly.express, plotly.graph_objects) генерирует JSON-описание графика, которое затем рендерится клиентской стороной. Это даёт два важных преимущества. Во-первых, графики легко встраиваются в веб-приложения: через Dash (фреймворк от тех же авторов), Streamlit или Jupyter Notebook (с расширением plotly), сохраняя полную интерактивность. Во-вторых, рендеринг переносится на сторону пользователя, что снижает нагрузку на сервер при генерации сложных дашбордов.

Plotly Express — высокоуровневый API, похожий по духу на Seaborn: px.scatter(df, x='x', y='y', color='group', size='value') создаёт интерактивную диаграмму рассеяния с цветовой и размерной кодировкой за одну строку. При этом результат можно легко донастроить через объектную модель (update_layout(), update_traces()), добавить анимацию (animation_frame), построить 3D-графики (px.scatter_3d) или географические карты (px.choropleth).

Единственное ограничение — объём данных. Поскольку интерактивность требует передачи данных в браузер, чрезмерно большие наборы (десятки тысяч точек) могут замедлять работу. Для таких случаев применяется агрегация на стороне Python (через Pandas или Datashader), либо используется серверный рендеринг в статичные изображения.